package org.xmlrpc.android;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URI;
import java.net.URL;
import java.util.Map;
import java.util.Vector;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;

import de.mtbnews.android.util.IBC;

import android.util.Log;

/**
 * XMLRPCClient allows to call remote XMLRPC method.
 * 
 * <p>
 * The following table shows how XML-RPC types are mapped to java call
 * parameters/response values.
 * </p>
 * 
 * <p>
 * <table border="2" align="center" cellpadding="5">
 * <thead>
 * <tr>
 * <th>XML-RPC Type</th>
 * <th>Call Parameters</th>
 * <th>Call Response</th>
 * </tr>
 * </thead>
 * 
 * <tbody>
 * <td>int, i4</td>
 * <td>byte<br />
 * Byte<br />
 * short<br />
 * Short<br />
 * int<br />
 * Integer</td>
 * <td>int<br />
 * Integer</td>
 * </tr>
 * <tr>
 * <td>i8</td>
 * <td>long<br />
 * Long</td>
 * <td>long<br />
 * Long</td>
 * </tr>
 * <tr>
 * <td>double</td>
 * <td>float<br />
 * Float<br />
 * double<br />
 * Double</td>
 * <td>double<br />
 * Double</td>
 * </tr>
 * <tr>
 * <td>string</td>
 * <td>String</td>
 * <td>String</td>
 * </tr>
 * <tr>
 * <td>boolean</td>
 * <td>boolean<br />
 * Boolean</td>
 * <td>boolean<br />
 * Boolean</td>
 * </tr>
 * <tr>
 * <td>dateTime.iso8601</td>
 * <td>java.util.Date<br />
 * java.util.Calendar</td>
 * <td>java.util.Date</td>
 * </tr>
 * <tr>
 * <td>base64</td>
 * <td>byte[]</td>
 * <td>byte[]</td>
 * </tr>
 * <tr>
 * <td>array</td>
 * <td>java.util.List&lt;Object&gt;<br />
 * Object[]</td>
 * <td>Object[]</td>
 * </tr>
 * <tr>
 * <td>struct</td>
 * <td>java.util.Map&lt;String, Object&gt;</td>
 * <td>java.util.Map&lt;String, Object&gt;</td>
 * </tr>
 * </tbody>
 * </table>
 * </p>
 * <p>
 * You can also pass as a parameter any object implementing XMLRPCSerializable
 * interface. In this case your object overrides getSerializable() telling how
 * to serialize to XMLRPC protocol
 * </p>
 */

public class XMLRPCClient extends XMLRPCCommon
{
	private HttpClient client;
	private HttpPost postMethod;
	private HttpParams httpParams;
	// These variables used in the code inspired by erickok in issue #6
	private boolean httpPreAuth = false;
	private String username = "";
	private String password = "";

	public CookieStore cookieStore = new BasicCookieStore();
	HttpContext localContext = new BasicHttpContext();

	/**
	 * XMLRPCClient constructor. Creates new instance based on server URI (Code
	 * contributed by sgayda2 from issue #17, and by erickok from ticket #10)
	 * 
	 * @param XMLRPC
	 *            server URI
	 */
	public XMLRPCClient(URI uri)
	{

		localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);

		SchemeRegistry registry = new SchemeRegistry();
		registry.register(new Scheme("http", new PlainSocketFactory(), 80));
		registry.register(new Scheme("https", SSLSocketFactory
				.getSocketFactory(), 443));

		postMethod = new HttpPost(uri);
		postMethod.addHeader("Content-Type", "text/xml");

		// WARNING
		// I had to disable "Expect: 100-Continue" header since I had
		// two second delay between sending http POST request and POST body
		httpParams = postMethod.getParams();
		HttpProtocolParams.setUseExpectContinue(httpParams, false);
		this.client = new DefaultHttpClient(new ThreadSafeClientConnManager(
				httpParams, registry), httpParams);
	}

	/**
	 * XMLRPCClient constructor. Creates new instance based on server URI (Code
	 * contributed by sgayda2 from issue #17)
	 * 
	 * @param XMLRPC
	 *            server URI
	 * @param HttpClient
	 *            to use
	 */

	public XMLRPCClient(URI uri, HttpClient client)
	{
		postMethod = new HttpPost(uri);
		postMethod.addHeader("Content-Type", "text/xml");

		// WARNING
		// I had to disable "Expect: 100-Continue" header since I had
		// two second delay between sending http POST request and POST body
		httpParams = postMethod.getParams();
		HttpProtocolParams.setUseExpectContinue(httpParams, false);
		this.client = client;
	}

	/**
	 * Amends user agent (Code contributed by mortenholdflod from issue #28)
	 * 
	 * @param userAgent
	 *            defining the new User Agent string
	 */
	public void setUserAgent(String userAgent)
	{
		postMethod.removeHeaders("User-Agent");
		postMethod.addHeader("User-Agent", userAgent);
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server address
	 */
	public XMLRPCClient(String url)
	{
		this(URI.create(url));
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server address
	 * @param HttpClient
	 *            to use
	 */
	public XMLRPCClient(String url, HttpClient client)
	{
		this(URI.create(url), client);
	}

	/**
	 * Convenience XMLRPCClient constructor. Creates new instance based on
	 * server URL
	 * 
	 * @param XMLRPC
	 *            server URL
	 */
	public XMLRPCClient(URL url)
	{
		this(URI.create(url.toExternalForm()));
	}

	/**
	 * Convenience XMLRPCClient constructor. Creates new instance based on
	 * server URL
	 * 
	 * @param XMLRPC
	 *            server URL
	 * @param HttpClient
	 *            to use
	 */
	public XMLRPCClient(URL url, HttpClient client)
	{
		this(URI.create(url.toExternalForm()), client);
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server address
	 * @param HTTP
	 *            Server - Basic Authentication - Username
	 * @param HTTP
	 *            Server - Basic Authentication - Password
	 */
	public XMLRPCClient(URI uri, String username, String password)
	{
		this(uri);

		((DefaultHttpClient) client).getCredentialsProvider()
				.setCredentials(
						new AuthScope(uri.getHost(), uri.getPort(),
								AuthScope.ANY_REALM),
						new UsernamePasswordCredentials(username, password));
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server address
	 * @param HTTP
	 *            Server - Basic Authentication - Username
	 * @param HTTP
	 *            Server - Basic Authentication - Password
	 * @param HttpClient
	 *            to use
	 */
	public XMLRPCClient(URI uri, String username, String password,
			HttpClient client)
	{
		this(uri, client);

		((DefaultHttpClient) this.client).getCredentialsProvider()
				.setCredentials(
						new AuthScope(uri.getHost(), uri.getPort(),
								AuthScope.ANY_REALM),
						new UsernamePasswordCredentials(username, password));
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server address
	 * @param HTTP
	 *            Server - Basic Authentication - Username
	 * @param HTTP
	 *            Server - Basic Authentication - Password
	 */
	public XMLRPCClient(String url, String username, String password)
	{
		this(URI.create(url), username, password);
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server address
	 * @param HTTP
	 *            Server - Basic Authentication - Username
	 * @param HTTP
	 *            Server - Basic Authentication - Password
	 * @param HttpClient
	 *            to use
	 */
	public XMLRPCClient(String url, String username, String password,
			HttpClient client)
	{
		this(URI.create(url), username, password, client);
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server url
	 * @param HTTP
	 *            Server - Basic Authentication - Username
	 * @param HTTP
	 *            Server - Basic Authentication - Password
	 */
	public XMLRPCClient(URL url, String username, String password)
	{
		this(URI.create(url.toExternalForm()), username, password);
	}

	/**
	 * Convenience constructor. Creates new instance based on server String
	 * address
	 * 
	 * @param XMLRPC
	 *            server url
	 * @param HTTP
	 *            Server - Basic Authentication - Username
	 * @param HTTP
	 *            Server - Basic Authentication - Password
	 * @param HttpClient
	 *            to use
	 */
	public XMLRPCClient(URL url, String username, String password,
			HttpClient client)
	{
		this(URI.create(url.toExternalForm()), username, password, client);
	}

	/**
	 * Sets basic authentication on web request using plain credentials
	 * 
	 * @param username
	 *            The plain text username
	 * @param password
	 *            The plain text password
	 * @param doPreemptiveAuth
	 *            Select here whether to authenticate without it being requested
	 *            first by the server.
	 */
	public void setBasicAuthentication(String username, String password,
			boolean doPreemptiveAuth)
	{
		// This code required to trigger the patch created by erickok in issue
		// #6
		if (doPreemptiveAuth = true)
		{
			this.httpPreAuth = doPreemptiveAuth;
			this.username = username;
			this.password = password;
		}
		else
		{
			((DefaultHttpClient) client)
					.getCredentialsProvider()
					.setCredentials(
							new AuthScope(postMethod.getURI().getHost(),
									postMethod.getURI().getPort(),
									AuthScope.ANY_REALM),
							new UsernamePasswordCredentials(username, password));
		}
	}

	/**
	 * Convenience Constructor: Sets basic authentication on web request using
	 * plain credentials
	 * 
	 * @param username
	 *            The plain text username
	 * @param password
	 *            The plain text password
	 */
	public void setBasicAuthentication(String username, String password)
	{
		setBasicAuthentication(username, password, false);
	}

	/**
	 * Call method with optional parameters. This is general method. If you want
	 * to call your method with 0-8 parameters, you can use more convenience
	 * call() methods
	 * 
	 * @param method
	 *            name of method to call
	 * @param params
	 *            parameters to pass to method (may be null if method has no
	 *            parameters)
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	@SuppressWarnings("unchecked")
	public Object callEx(String method, Object[] params) throws XMLRPCException
	{
		try
		{
			// prepare POST body
			String body = methodCall(method, params);

			// set POST body
			HttpEntity entity = new StringEntity(body);
			postMethod.setEntity(entity);

			// This code slightly tweaked from the code by erickok in issue #6
			// Force preemptive authentication
			// This makes sure there is an 'Authentication: ' header being send
			// before trying and failing and retrying
			// by the basic authentication mechanism of DefaultHttpClient
			if (this.httpPreAuth == true)
			{
				String auth = this.username + ":" + this.password;
				postMethod.addHeader("Authorization", "Basic "
						+ Base64Coder.encode(auth.getBytes()).toString());
			}

			// Log.d(Tag.LOG, "ros HTTP POST");
			// execute HTTP POST request

			Log.d("IBC", "XMLRPC method: " + method);
			// for (Cookie cookie : cookieStore.getCookies())
			// {
			// Log.d("IBC", cookie.toString());
			// }

			HttpResponse response = client.execute(postMethod, localContext);
			// Log.d(Tag.LOG, "ros HTTP POSTed");

			// check status code
			int statusCode = response.getStatusLine().getStatusCode();

			// Log.d("IBC", "Status: " + statusCode);
			// for (Cookie cookie : cookieStore.getCookies())
			// {
			// Log.d("IBC", cookie.toString());
			// }
			// Log.d("IBC", "");
			
			// Log.d(Tag.LOG, "ros status code:" + statusCode);
			if (statusCode != HttpStatus.SC_OK)
			{
				throw new XMLRPCException("HTTP status code: " + statusCode
						+ " != " + HttpStatus.SC_OK, statusCode);
			}

			// parse response stuff
			//
			// setup pull parser
			XmlPullParser pullParser = XmlPullParserFactory.newInstance()
					.newPullParser();
			entity = response.getEntity();
			Reader reader = new InputStreamReader(new BufferedInputStream(
					entity.getContent(), 250));
			// for testing purposes only
			// reader = new
			// StringReader("<?xml version='1.0'?><methodResponse><params><param><value>\n\n\n</value></param></params></methodResponse>");
			pullParser.setInput(reader);

			// lets start pulling...
			pullParser.nextTag();

			// Log.d("XMLRPC response",this.inputStreamToString(entity.getContent()));
			// entity.getContent().reset();

			pullParser.require(XmlPullParser.START_TAG, null,
					Tag.METHOD_RESPONSE);

			pullParser.nextTag(); // either Tag.PARAMS (<params>) or Tag.FAULT
			// (<fault>)
			String tag = pullParser.getName();

			if (tag.equals(Tag.PARAMS))
			{
				// normal response
				pullParser.nextTag(); // Tag.PARAM (<param>)
				pullParser.require(XmlPullParser.START_TAG, null, Tag.PARAM);
				pullParser.nextTag(); // Tag.VALUE (<value>)
				// no parser.require() here since its called in
				// XMLRPCSerializer.deserialize() below

				// deserialize result
				Object obj = iXMLRPCSerializer.deserialize(pullParser);
				entity.consumeContent();
				return obj;
			}
			else if (tag.equals(Tag.FAULT))
			{
				// fault response
				pullParser.nextTag(); // Tag.VALUE (<value>)
				// no parser.require() here since its called in
				// XMLRPCSerializer.deserialize() below

				// deserialize fault result
				Map<String, Object> map = (Map<String, Object>) iXMLRPCSerializer
						.deserialize(pullParser);
				String faultString = (String) map.get(Tag.FAULT_STRING);
				int faultCode = (Integer) map.get(Tag.FAULT_CODE);
				entity.consumeContent();
				throw new XMLRPCFault(faultString, faultCode);
			}
			else
			{
				entity.consumeContent();
				throw new XMLRPCException("Bad tag <" + tag
						+ "> in XMLRPC response - neither <params> nor <fault>");
			}
		}
		catch (XMLRPCException e)
		{
			// catch & propagate XMLRPCException/XMLRPCFault
			throw e;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			// wrap any other Exception(s) around XMLRPCException
			throw new XMLRPCException(e);
		}
	}

	private String methodCall(String method, Object[] params)
			throws IllegalArgumentException, IllegalStateException, IOException
	{
		StringWriter bodyWriter = new StringWriter();
		serializer.setOutput(bodyWriter);
		serializer.startDocument(null, null);
		serializer.startTag(null, Tag.METHOD_CALL);
		// set method name
		serializer.startTag(null, Tag.METHOD_NAME).text(method).endTag(null,
				Tag.METHOD_NAME);

		serializeParams(params);

		serializer.endTag(null, Tag.METHOD_CALL);
		serializer.endDocument();

		return bodyWriter.toString();
	}

	/**
	 * Convenience method call with no parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method) throws XMLRPCException
	{
		return callEx(method, null);
	}

	/**
	 * Convenience method call with a vectorized parameter (Code contributed by
	 * jahbromo from issue #14)
	 * 
	 * @param method
	 *            name of method to call
	 * @param paramsv
	 *            vector of method's parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */

	public Object call(String method, Vector paramsv) throws XMLRPCException
	{
		Object[] params = new Object[paramsv.size()];
		for (int i = 0; i < paramsv.size(); i++)
		{
			params[i] = paramsv.elementAt(i);
		}
		return callEx(method, params);
	}

	/**
	 * Convenience method call with one parameter
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0) throws XMLRPCException
	{
		Object[] params = { p0, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with two parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1)
			throws XMLRPCException
	{
		Object[] params = { p0, p1, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with three parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @param p2
	 *            method's 3rd parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1, Object p2)
			throws XMLRPCException
	{
		Object[] params = { p0, p1, p2, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with four parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @param p2
	 *            method's 3rd parameter
	 * @param p3
	 *            method's 4th parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1, Object p2, Object p3)
			throws XMLRPCException
	{
		Object[] params = { p0, p1, p2, p3, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with five parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @param p2
	 *            method's 3rd parameter
	 * @param p3
	 *            method's 4th parameter
	 * @param p4
	 *            method's 5th parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1, Object p2,
			Object p3, Object p4) throws XMLRPCException
	{
		Object[] params = { p0, p1, p2, p3, p4, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with six parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @param p2
	 *            method's 3rd parameter
	 * @param p3
	 *            method's 4th parameter
	 * @param p4
	 *            method's 5th parameter
	 * @param p5
	 *            method's 6th parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1, Object p2,
			Object p3, Object p4, Object p5) throws XMLRPCException
	{
		Object[] params = { p0, p1, p2, p3, p4, p5, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with seven parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @param p2
	 *            method's 3rd parameter
	 * @param p3
	 *            method's 4th parameter
	 * @param p4
	 *            method's 5th parameter
	 * @param p5
	 *            method's 6th parameter
	 * @param p6
	 *            method's 7th parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1, Object p2,
			Object p3, Object p4, Object p5, Object p6) throws XMLRPCException
	{
		Object[] params = { p0, p1, p2, p3, p4, p5, p6, };
		return callEx(method, params);
	}

	/**
	 * Convenience method call with eight parameters
	 * 
	 * @param method
	 *            name of method to call
	 * @param p0
	 *            method's 1st parameter
	 * @param p1
	 *            method's 2nd parameter
	 * @param p2
	 *            method's 3rd parameter
	 * @param p3
	 *            method's 4th parameter
	 * @param p4
	 *            method's 5th parameter
	 * @param p5
	 *            method's 6th parameter
	 * @param p6
	 *            method's 7th parameter
	 * @param p7
	 *            method's 8th parameter
	 * @return deserialized method return value
	 * @throws XMLRPCException
	 */
	public Object call(String method, Object p0, Object p1, Object p2,
			Object p3, Object p4, Object p5, Object p6, Object p7)
			throws XMLRPCException
	{
		Object[] params = { p0, p1, p2, p3, p4, p5, p6, p7, };
		return callEx(method, params);
	}
}
